Improve agent and scenario forms

Allow to configure agent event target
Make select2 agent tags clickable

Dominik Sander 8 years ago
parent
commit
53d4559f6e

+ 6 - 0
app/assets/javascripts/components/core.js.coffee

@@ -16,6 +16,12 @@ $ ->
16 16
   # Select2 Selects
17 17
   $(".select2").select2(width: 'resolve')
18 18
 
19
+  $(".select2-linked-tags").select2(
20
+    width: 'resolve',
21
+    formatSelection: (obj) ->
22
+      "<a href=\"#{this.element.data('urlPrefix')}/#{obj.id}/edit\" onClick=\"Utils.select2TagClickHandler(event, this)\">#{Utils.escape(obj.text)}</a>"
23
+  )
24
+
19 25
   # Helper for selecting text when clicked
20 26
   $('.selectable-text').each ->
21 27
     $(this).click ->

+ 29 - 0
app/assets/javascripts/components/utils.js.coffee

@@ -132,3 +132,32 @@ class @Utils
132 132
       .fail (xhr, status, error) ->
133 133
         alert('Error: ' + error)
134 134
         callback()
135
+
136
+  @select2TagClickHandler: (e, elem) ->
137
+    if e.which == 1
138
+      window.location = $(elem).attr('href')
139
+    else
140
+      window.open($(elem).attr('href'))
141
+
142
+  # _.escape from underscore: https://github.com/jashkenas/underscore/blob/1e68f06610fa4ecb7f2c45d1eb2ad0173d6a2cc1/underscore.js#L1411-L1436
143
+  escapeMap =
144
+    '&': '&amp;'
145
+    '<': '&lt;'
146
+    '>': '&gt;'
147
+    '"': '&quot;'
148
+    '\'': '&#x27;'
149
+    '`': '&#x60;'
150
+
151
+  createEscaper = (map) ->
152
+    escaper = (match) ->
153
+      map[match]
154
+
155
+    # Regexes for identifying a key that needs to be escaped.
156
+    source = '(?:' + Object.keys(map).join('|') + ')'
157
+    testRegexp = RegExp(source)
158
+    replaceRegexp = RegExp(source, 'g')
159
+    (string) ->
160
+      string = if string == null then '' else '' + string
161
+      if testRegexp.test(string) then string.replace(replaceRegexp, escaper) else string
162
+
163
+  @escape = createEscaper(escapeMap)

+ 4 - 2
app/assets/javascripts/pages/agent-edit-page.js.coffee

@@ -117,10 +117,12 @@ class @AgentEditPage
117 117
     $(".control-link-region").show()
118 118
 
119 119
   hideEventCreation: ->
120
-    $(".event-related-region").hide()
120
+    $(".event-related-region .select2-container").hide()
121
+    $(".event-related-region .cannot-create-events").show()
121 122
 
122 123
   showEventCreation: ->
123
-    $(".event-related-region").show()
124
+    $(".event-related-region .select2-container").show()
125
+    $(".event-related-region .cannot-create-events").hide()
124 126
 
125 127
   showEventDescriptions: ->
126 128
     if $("#agent_source_ids").val()

+ 6 - 21
app/models/agent.rb

@@ -24,16 +24,17 @@ class Agent < ActiveRecord::Base
24 24
 
25 25
   EVENT_RETENTION_SCHEDULES = [["Forever", 0], ['1 hour', 1.hour], ['6 hours', 6.hours], ["1 day", 1.day], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n.days] })]
26 26
 
27
-  attr_accessible :options, :memory, :name, :type, :schedule, :controller_ids, :control_target_ids, :disabled, :source_ids, :scenario_ids, :keep_events_for, :propagate_immediately, :drop_pending_events
27
+  attr_accessible :options, :memory, :name, :type, :schedule, :controller_ids, :control_target_ids, :disabled, :source_ids, :receiver_ids, :scenario_ids, :keep_events_for, :propagate_immediately, :drop_pending_events
28 28
 
29 29
   json_serialize :options, :memory
30 30
 
31 31
   validates_presence_of :name, :user
32 32
   validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last)
33
-  validate :sources_are_owned
34
-  validate :controllers_are_owned
35
-  validate :control_targets_are_owned
36
-  validate :scenarios_are_owned
33
+  validates :sources, owned_by: :user_id
34
+  validates :receivers, owned_by: :user_id
35
+  validates :controllers, owned_by: :user_id
36
+  validates :control_targets, owned_by: :user_id
37
+  validates :scenarios, owned_by: :user_id
37 38
   validate :validate_schedule
38 39
   validate :validate_options
39 40
 
@@ -267,22 +268,6 @@ class Agent < ActiveRecord::Base
267 268
   
268 269
   private
269 270
   
270
-  def sources_are_owned
271
-    errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user_id == user_id }
272
-  end
273
-  
274
-  def controllers_are_owned
275
-    errors.add(:controllers, "must be owned by you") unless controllers.all? {|s| s.user_id == user_id }
276
-  end
277
-
278
-  def control_targets_are_owned
279
-    errors.add(:control_targets, "must be owned by you") unless control_targets.all? {|s| s.user_id == user_id }
280
-  end
281
-
282
-  def scenarios_are_owned
283
-    errors.add(:scenarios, "must be owned by you") unless scenarios.all? {|s| s.user_id == user_id }
284
-  end
285
-
286 271
   def validate_schedule
287 272
     unless cannot_be_scheduled?
288 273
       errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s)

+ 6 - 0
app/validators/owned_by_validator.rb

@@ -0,0 +1,6 @@
1
+class OwnedByValidator < ActiveModel::EachValidator
2
+  def validate_each(record, attribute, association)
3
+    return if association.all? {|s| s[options[:with]] == record[options[:with]] }
4
+    record.errors[attribute] << "must be owned by you"
5
+  end
6
+end

+ 17 - 3
app/views/agents/_form.html.erb

@@ -68,7 +68,7 @@
68 68
                   <%= f.select(:control_target_ids,
69 69
                                options_for_select(current_user.agents.map {|s| [s.name, s.id] },
70 70
                                                   @agent.control_target_ids),
71
-                               {}, { multiple: true, size: 5, class: 'select2 form-control' }) %>
71
+                               {}, { multiple: true, size: 5, class: 'select2-linked-tags form-control', data: {url_prefix: '/agents'}}) %>
72 72
                 </div>
73 73
               </div>
74 74
             </div>
@@ -83,12 +83,13 @@
83 83
 
84 84
             <div class="form-group">
85 85
               <%= f.label :sources %>
86
+              <span class="glyphicon glyphicon-question-sign hover-help" data-content="This Agent will receive events from the selected Agents."></span>
86 87
               <div class="link-region" data-can-receive-events="<%= @agent.can_receive_events? %>">
87 88
                 <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %>
88 89
                 <%= f.select(:source_ids,
89 90
                              options_for_select(eventSources.map {|s| [s.name, s.id] },
90 91
                                                 @agent.source_ids),
91
-                             {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
92
+                             {}, { :multiple => true, :size => 5, :class => 'select2-linked-tags form-control', data: {url_prefix: '/agents'} }) %>
92 93
                 <span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span>
93 94
                 <%= f.label :propagate_immediately, :class => 'propagate-immediately' do %>Propagate immediately
94 95
                   <%= f.check_box :propagate_immediately %>
@@ -98,13 +99,26 @@
98 99
               </div>
99 100
             </div>
100 101
 
102
+            <div class="form-group">
103
+              <%= f.label :receivers %>
104
+              <span class="glyphicon glyphicon-question-sign hover-help" data-content="Events created by this Agent will be sent to the selected Agents."></span>
105
+              <div class="event-related-region">
106
+                <% eventTargets = (current_user.agents - [@agent]).find_all { |a| a.can_receive_events? } %>
107
+                <%= f.select(:receiver_ids,
108
+                             options_for_select(eventTargets.map {|s| [s.name, s.id] },
109
+                                                @agent.receiver_ids),
110
+                             {}, { :multiple => true, :size => 5, :class => 'select2-linked-tags form-control', data: {url_prefix: '/agents'} }) %>
111
+                <span class='cannot-create-events text-info'>This type of Agent cannot create events.</span>
112
+              </div>
113
+            </div>
114
+
101 115
             <% if current_user.scenario_count > 0 %>
102 116
               <div class="form-group">
103 117
                 <%= f.label :scenarios %>
104 118
                 <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span>
105 119
                 <%= f.select(:scenario_ids,
106 120
                              options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids),
107
-                             {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
121
+                             {}, { :multiple => true, :size => 5, :class => 'select2-linked-tags form-control', data: {url_prefix: '/scenarios'} }) %>
108 122
               </div>
109 123
             <% end %>
110 124
 

+ 1 - 1
app/views/scenarios/_form.html.erb

@@ -53,7 +53,7 @@
53 53
           <%= f.label :agents %>
54 54
           <%= f.select(:agent_ids,
55 55
                        options_for_select(current_user.agents.pluck(:name, :id), @scenario.agent_ids),
56
-                       {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
56
+                       {}, { :multiple => true, :size => 5, :class => 'select2-linked-tags form-control', data: {url_prefix: '/agents'} }) %>
57 57
         </div>
58 58
       </div>
59 59
     </div>

+ 1 - 1
config/locales/en.yml

@@ -40,4 +40,4 @@ en:
40 40
         other: ">%{count}yr"
41 41
       almost_x_years:
42 42
         one:   "~1yr"
43
-        other: "~%{count}yr"
43
+        other: "~%{count}yr"

+ 12 - 0
spec/controllers/agents_controller_spec.rb

@@ -203,6 +203,18 @@ describe AgentsController do
203 203
       expect(assigns(:agent)).to be_a(Agents::WebsiteAgent)
204 204
     end
205 205
 
206
+    it "creates Agents and accepts specifing a target agent" do
207
+      sign_in users(:bob)
208
+      attributes = valid_attributes
209
+      attributes[:receiver_ids] = attributes[:source_ids]
210
+      expect {
211
+        expect {
212
+          post :create, :agent => attributes
213
+        }.to change { users(:bob).agents.count }.by(1)
214
+      }.to change { Link.count }.by(2)
215
+      expect(assigns(:agent)).to be_a(Agents::WebsiteAgent)
216
+    end
217
+
206 218
     it "shows errors" do
207 219
       sign_in users(:bob)
208 220
       expect {

+ 28 - 2
spec/features/create_an_agent_spec.rb

@@ -1,8 +1,11 @@
1 1
 require 'capybara_helper'
2 2
 
3 3
 describe "Creating a new agent", js: true do
4
-  it "creates an agent" do
4
+  before(:each) do
5 5
     login_as(users(:bob))
6
+  end
7
+
8
+  it "creates an agent" do
6 9
     visit "/"
7 10
     page.find("a", text: "Agents").trigger(:mouseover)
8 11
     click_on("New Agent")
@@ -15,7 +18,6 @@ describe "Creating a new agent", js: true do
15 18
   end
16 19
 
17 20
   it "creates an alert if a new agent with invalid json is submitted" do
18
-    login_as(users(:bob))
19 21
     visit "/"
20 22
     page.find("a", text: "Agents").trigger(:mouseover)
21 23
     click_on("New Agent")
@@ -30,4 +32,28 @@ describe "Creating a new agent", js: true do
30 32
     }')
31 33
     expect(get_alert_text_from { click_on "Save" }).to have_text("Sorry, there appears to be an error in your JSON input. Please fix it before continuing.")
32 34
   end
35
+
36
+  context "displaying the correct information" do
37
+    before(:each) do
38
+      visit new_agent_path
39
+    end
40
+    it "shows all options for agents that can be scheduled, create and receive events" do
41
+      select2("Website Agent", from: "Type")
42
+      expect(page).not_to have_content('This type of Agent cannot create events.')
43
+    end
44
+
45
+    it "does not show the target select2 field when the agent can not create events" do
46
+      select2("Growl Agent", from: "Type")
47
+      expect(page).to have_content('This type of Agent cannot create events.')
48
+    end
49
+  end
50
+
51
+  it "allows to click on on the agent name in select2 tags" do
52
+    agent = agents(:bob_weather_agent)
53
+    visit new_agent_path
54
+    select2("Website Agent", from: "Type")
55
+    select2("SF Weather", from: 'Sources')
56
+    click_on "SF Weather"
57
+    expect(page).to have_content "Editing your WeatherAgent"
58
+  end
33 59
 end

+ 2 - 0
spec/helpers/dot_helper_spec.rb

@@ -51,6 +51,8 @@ describe DotHelper do
51 51
             agent.save!
52 52
           },
53 53
         ]
54
+        @foo.reload
55
+        @bar2.reload
54 56
       end
55 57
 
56 58
       it "generates a DOT script" do

+ 13 - 0
spec/models/agent_spec.rb

@@ -546,6 +546,17 @@ describe Agent do
546 546
         expect(agent).to have(0).errors_on(:sources)
547 547
       end
548 548
 
549
+      it "should not allow target agents owned by other people" do
550
+        agent = Agents::SomethingSource.new(:name => "something")
551
+        agent.user = users(:bob)
552
+        agent.receiver_ids = [agents(:bob_weather_agent).id]
553
+        expect(agent).to have(0).errors_on(:receivers)
554
+        agent.receiver_ids = [agents(:jane_weather_agent).id]
555
+        expect(agent).to have(1).errors_on(:receivers)
556
+        agent.user = users(:jane)
557
+        expect(agent).to have(0).errors_on(:receivers)
558
+      end
559
+
549 560
       it "should not allow controller agents owned by other people" do
550 561
         agent = Agents::SomethingSource.new(:name => "something")
551 562
         agent.user = users(:bob)
@@ -961,6 +972,8 @@ describe AgentDrop do
961 972
     @efa.sources << @wsa1 << @wsa2
962 973
     @efa.memory[:test] = 1
963 974
     @efa.save!
975
+    @wsa1.reload
976
+    @wsa2.reload
964 977
   end
965 978
 
966 979
   it 'should be created via Agent#to_liquid' do